package com.nebula.core.dsl;

import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLExpr;
import com.alibaba.druid.sql.ast.SQLObject;
import com.alibaba.druid.sql.ast.expr.*;
import com.alibaba.druid.sql.ast.statement.SQLSelectGroupByClause;
import com.alibaba.druid.sql.parser.Token;
import com.alibaba.druid.sql.visitor.SQLASTOutputVisitor;
import com.nebula.core.constant.Constant;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author zhangtao
 */
public class WhereStatement {
    private SQLExpr where;
    private SQLSelectGroupByClause groupBy;

    private WhereStatement() {
    }

    public static WhereStatement create() {
        return new WhereStatement();
    }

    public WhereStatement and(SQLExpr sqlExpr) {
        append(sqlExpr, SQLBinaryOperator.BooleanAnd);
        return this;
    }

    public WhereStatement and(String sqlExpr) {
        and(SQLUtils.toMySqlExpr(sqlExpr));
        return this;
    }

    public WhereStatement and(WhereStatement builder) {
        append(builder.build(), SQLBinaryOperator.BooleanAnd);
        return this;
    }

    public WhereStatement andFunction(String functionName, String... parameters) {
        and(createFunction(functionName, parameters));
        return this;
    }

    private SQLMethodInvokeExpr createFunction(String functionName, String... parameters) {
        SQLMethodInvokeExpr func = new SQLMethodInvokeExpr(functionName);
        for (String parameter : parameters) {
            func.addArgument(new SQLIdentifierExpr(StringUtils.wrap(parameter, "'")));
        }
        return func;
    }

    public WhereStatement andFunction(String functionName, WhereStatement where, String... parameters) {
        and(createFunction(functionName, where, parameters));
        return this;
    }

    private SQLMethodInvokeExpr createFunction(String functionName, WhereStatement where, String... parameters) {
        SQLMethodInvokeExpr func = new SQLMethodInvokeExpr(functionName);
        func.addArgument(where.build());
        for (String parameter : parameters) {
            func.addArgument(new SQLIdentifierExpr(StringUtils.wrap(parameter, "'")));
        }
        return func;
    }

    public WhereStatement andFunction(String functionName, WhereStatement where) {
        and(createFunction(functionName, where));
        return this;
    }

    private SQLMethodInvokeExpr createFunction(String functionName, WhereStatement where) {
        SQLMethodInvokeExpr func = new SQLMethodInvokeExpr(functionName);
        func.addArgument(where.build());
        return func;
    }

    public WhereStatement andWithIn(String var, Collection<?> values) {
        if (CollectionUtils.isEmpty(values)) {
            return this;
        }
        append(in(var, values), SQLBinaryOperator.BooleanAnd);
        return this;
    }

    public WhereStatement and(String key, String operator, Object value) {
        if (value == null) {
            return this;
        }
        if (value instanceof String) {
            String str = (String) value;
            if (StringUtils.isBlank(str)) {
                return this;
            }
        }
        String operatorUpper = operator.toUpperCase();
        switch (operatorUpper) {
            case "IN":
                if (value instanceof List<?>) {
                    return and(in(key, (List<?>) value));
                }
                return and(in(key, Arrays.asList(value.toString().split(Token.COMMA.name))));
            case "NOT IN":
                if (value instanceof List<?>) {
                    return and(notIn(key, (List<?>) value));
                }
                return and(notIn(key, Arrays.asList(value.toString().split(Token.COMMA.name))));
            case "NOT CONTAINS":
                return and(notCondition(key, getBinaryOperator(operatorUpper), value));
            case "OR":
                return andWithIn(key, Arrays.asList(value.toString().split(Token.COMMA.name)));
            default:
                return and(condition(key, getBinaryOperator(operatorUpper), value));
        }
    }

    private SQLBinaryOperator getBinaryOperator(String operator) {
        switch (operator) {
            case "IS": {
                return SQLBinaryOperator.Is;
            }
            case "IS NOT": {
                return SQLBinaryOperator.IsNot;
            }
            case "==":
//                return SQLBinaryOperator.Assignment;
            case "=":
            case "CONTAINS":
            case "NOT CONTAINS": {
                return SQLBinaryOperator.Equality;
            }
            case "!=": {
                return SQLBinaryOperator.NotEqual;
            }
            case ">": {
                return SQLBinaryOperator.GreaterThan;
            }
            case ">=": {
                return SQLBinaryOperator.GreaterThanOrEqual;
            }
            case "<": {
                return SQLBinaryOperator.LessThan;
            }
            case "<=": {
                return SQLBinaryOperator.LessThanOrEqual;
            }
            default: {
                throw new IllegalArgumentException("wrong operator" + operator);
            }
        }
    }

    public WhereStatement or(WhereStatement builder) {
        append(builder.build(), SQLBinaryOperator.BooleanOr);
        return this;
    }

    public WhereStatement or(SQLExpr sqlExpr) {
        append(sqlExpr, SQLBinaryOperator.BooleanOr);
        return this;
    }

    public WhereStatement or(String sqlExpr) {
        or(SQLUtils.toMySqlExpr(sqlExpr));
        return this;
    }

    public WhereStatement or(String key, String operator, Object value) {
        if (value == null) {
            return this;
        }
        if (value instanceof String) {
            String str = (String) value;
            if (StringUtils.isBlank(str)) {
                return this;
            }
        }
        String operatorUpper = operator.toUpperCase();
        switch (operatorUpper) {
            case "IN":
                if (value instanceof List<?>) {
                    return orWithIn(key, (List<?>) value);
                }
                return orWithIn(key, Arrays.asList(value.toString().split(Token.COMMA.name)));
            case "NOT IN":
                if (value instanceof List<?>) {
                    return orWithNotIn(key, (List<?>) value);
                }
                return orWithNotIn(key, Arrays.asList(value.toString().split(Token.COMMA.name)));
            case "NOT CONTAINS":
                return or(notCondition(key, getBinaryOperator(operatorUpper), value));
            default:
                return or(condition(key, getBinaryOperator(operatorUpper), value));
        }
    }

    public WhereStatement orWithIn(String var, List<?> values) {
        if (CollectionUtils.isEmpty(values)) {
            return this;
        }
        append(in(var, values), SQLBinaryOperator.BooleanOr);
        return this;
    }

    public WhereStatement orWithNotIn(String var, List<?> values) {
        if (CollectionUtils.isEmpty(values)) {
            return this;
        }
        append(notIn(var, values), SQLBinaryOperator.BooleanOr);
        return this;
    }

    public SQLExpr build() {
        return where;
    }

    @Override
    public String toString() {
        return toSqlString(where);
    }

    public static String toSqlString(SQLObject sqlObject) {
        StringBuilder out = new StringBuilder();
        SQLASTOutputVisitor visitor = new NebulaOutputVisitor(out);

        SQLUtils.FormatOption option = SQLUtils.DEFAULT_FORMAT_OPTION;
        visitor.setUppCase(option.isUppCase());
        visitor.setPrettyFormat(option.isPrettyFormat());
        visitor.setParameterized(option.isParameterized());
        sqlObject.accept(visitor);
        return out.toString();
    }

    private SQLBinaryOpExpr condition(String var, SQLBinaryOperator operator, Object val) {
        SQLBinaryOpExpr condition = new SQLBinaryOpExpr();
        condition.setLeft(new SQLIdentifierExpr(var));
        condition.setOperator(operator);
        condition.setRight(convert(val));
        return condition;
    }

    private SQLNotExpr notCondition(String var, SQLBinaryOperator operator, Object val) {
        SQLBinaryOpExpr condition = new SQLBinaryOpExpr();
        condition.setLeft(new SQLIdentifierExpr(var));
        condition.setOperator(operator);
        condition.setRight(convert(val));
        return new SQLNotExpr(condition);
    }

    private SQLInListExpr in(String var, Collection<?> values) {
        SQLInListExpr sqlInListExpr = new SQLInListExpr();
        sqlInListExpr.setExpr(new SQLIdentifierExpr(var));
        sqlInListExpr.setTargetList(values.stream().map(this::convert).collect(Collectors.toList()));
        return sqlInListExpr;
    }

    private SQLInListExpr notIn(String var, Collection<?> values) {
        SQLInListExpr sqlInListExpr = new SQLInListExpr();
        sqlInListExpr.setExpr(new SQLIdentifierExpr(var));
        sqlInListExpr.setNot(!sqlInListExpr.isNot());
        sqlInListExpr.setTargetList(values.stream().map(this::convert).collect(Collectors.toList()));
        return sqlInListExpr;
    }


    private SQLExpr convert(Object value) {
        if (value instanceof Number) {
            return new SQLNumberExpr((Number) value);
        } else if (value instanceof String) {
            String str = (String) value;
            if (Constant.NULL.equalsIgnoreCase(str)) {
                return new SQLNullExpr();
            } else {
                return new SQLCharExpr(str);
            }
        } else if (value instanceof Boolean) {
            return new SQLBooleanExpr((Boolean) value);
        } else if (value instanceof SQLExpr) {
            return (SQLExpr) value;
        }
        return new SQLCharExpr();
    }


    private void append(SQLExpr expr, SQLBinaryOperator logic) {
        if (where == null) {
            where = expr;
        } else {
            SQLBinaryOpExpr parent = new SQLBinaryOpExpr();
            parent.setLeft(where);
            parent.setOperator(logic);
            parent.setRight(expr);
            where = parent;
        }
    }
}
